import curses
import datetime
import bisect
import logging

def max_key_length(obj):
    return max([len(key) for key in [obj[i]['key'] for i in range(0, len(obj))]])

def today():
    return datetime.date.today().strftime('%Y-%m-%d')

class CommandWindow():
    def __init__(self, max_nl, max_nc, y, x):
        self.max_nl = max_nl
        self.max_nc = max_nc
        self.y = y
        self.x = x

        self.window = curses.newwin(max_nl, max_nc, y, x)
        self.textbox = curses.textpad.Textbox(self.window)

        self.text = None
        self.search_query = None
    
    def update(self, state, keypress):
        if keypress == curses.KEY_RESIZE:
            max_nl, max_nc = state['stdscr'].getmaxyx()
            self.max_nc = max_nc
            self.window.mvwin(max_nl - 1, 0)
        if state['write']:
            filepath = state['filepath']
            state['write'] = False
            state['write_callback'](filepath, state['logs'])
            self.text = f'file "{filepath}" written'

    def render(self):
        self.window.clear()
        self.window.addstr(0, 0, self.text[:self.max_nc] if self.text else '', curses.A_REVERSE)
        self.window.refresh()
        
    def clear(self):
        self.text = None

    def enter(self, prefix):
        self.window.erase()
        self.window.addstr(0, 0, prefix)
        self.textbox.edit()
        self.text = self.textbox.gather()
        return self.text

class WindowTreeNode():
    def __init__(self, cmd, max_nl, max_nc, nl, nc, y, x, entry, parent=None, active=False, display_metadata=False):
        self.cmd = cmd
        self.max_nl = max_nl
        self.max_nc = max_nc
        self.nl = nl
        self.nc = nc
        self.y = y
        self.x = x
        self.window = curses.newwin(nl, nc, y, x)
        self.entry = entry

        self.parent = parent
        self.active = active
        self.display_metadata = display_metadata

        self.children = []
        self.cursor_pos = 0
        self.deletion_queue = []
        self.metadata_columns = 4 if display_metadata else 0

        self.key = self.entry['key']
        self.items = self.entry['items']
        self.leaf = self.entry['leaf']
        self.sorting = self.entry['sorting']
        self.comment = self.entry['comment']

    def deactivate(self):
        self.active = False

    def activate(self):
        self.active = True

    def focus(self):
        self.nc = self.max_nc - (self.parent.nc if self.parent else 0)
        self.window.resize(self.nl, self.nc)

    def defocus(self):
        self.nc = max_key_length(self.items)
        self.window.resize(self.nl, self.nc)
    
    def selected(self):
        return None if len(self.items) == 0 else self.items[self.cursor_pos]

    def get_key(self, item):
        return item if self.leaf else item['key']

    def add_child_window(self):
        self.defocus()

        sel = self.selected()
        if self.leaf or not sel:
            return

        child = WindowTreeNode(
            cmd=self.cmd,
            max_nl=self.max_nl,
            max_nc=self.max_nc,
            nl=self.nl,
            nc=self.max_nc - self.nc - 2,
            y=self.y,
            x=self.x + self.nc + 2,
            entry=sel,
            parent=self,
            active=True,
            display_metadata=self.display_metadata)
        self.children.append(child)

    def remove_child_window(self, window):
        self.focus()

        self.children.remove(window)

    def build_dict_entry(self, key):
        return {'key': key, 'leaf': True, 'sorting': True, 'comment': '', 'items': []}

    def notify_cmd(self, text):
        self.cmd.clear()
        self.cmd.text = text

    def query_cmd(self, prefix, init_text=''):
        self.cmd.clear()
        entered = self.cmd.enter(f'{prefix}{init_text}')
        return entered[len(prefix):].rstrip()
    
    def search(self, query):
        # TODO: support reverse search to implement [n]ext/[N]ext functionality
        if self.leaf:
            self.search_query = query
            for i in range(self.cursor_pos, len(self.items)):
                if query in self.items[i]:
                    self.cursor_pos = i
                    return
            for i in range(0, self.cursor_pos):
                if query in self.items[i]:
                    self.cursor_pos = i
                    return
            self.notify_cmd(f'search for {query}: not found')

    def update_entry(self, new_entry):
        if len(new_entry) == 0:
            return

        sel = self.selected()
        if self.leaf:
            self.items.remove(sel)
            self.insert_entry(new_entry)
        else:
            sel['key'] = new_entry

    def insert_entry(self, new_entry, after=False):
        if len(new_entry) == 0:
            return

        if len(self.items) == 0:
            choice_leaf = self.query_cmd('insert as new category? [y/N] ')
            choice_sorting = self.query_cmd('create list with sorting? [y/N] ')
            self.sorting = self.entry['sorting'] = 'y' in choice_sorting.lower()
            if 'y' in choice_leaf.lower():
                self.items.append(self.build_dict_entry(new_entry))
                self.leaf = self.entry['leaf'] = False
            else:
                self.items.append(new_entry)
        elif self.leaf:
            if self.sorting:
                bisect.insort(self.items, new_entry)
                self.cursor_pos = bisect.bisect_left(self.items, new_entry)
            else:
                new_pos = self.cursor_pos if not after else self.cursor_pos + 1
                self.items.insert(new_pos, new_entry)
                self.cursor_pos = new_pos
        else:
            new_dict = self.build_dict_entry(new_entry)
            new_pos = self.cursor_pos if not after else self.cursor_pos + 1
            self.items.insert(new_pos, new_dict)
            self.cursor_pos = new_pos

    def soft_delete_selected_entry(self):
        if self.cursor_pos not in self.deletion_queue:
            self.deletion_queue.append(self.cursor_pos)

    def hard_delete_entries(self):
        indices = sorted(self.deletion_queue, reverse=True)
        for pos in indices:
            del self.items[pos]
        self.deletion_queue.clear()
        self.cursor_pos = min(self.cursor_pos, len(self.items) - 1)
    
    def undo_soft_delete(self):
        if len(self.deletion_queue) > 0:
            self.deletion_queue.pop()

    def toggle_sorting(self):
        self.sorting = self.entry['sorting'] = not self.sorting
        self.notify_cmd(f'sorting toggled to {self.sorting}')

    def update(self, state, keypress):
        if keypress == curses.KEY_RESIZE:
            max_nl, max_nc = state['stdscr'].getmaxyx()
            # subtract cmd window
            max_nl -= 1
            diff_l = max_nl - self.max_nl
            diff_c = max_nc - self.max_nc
            self.max_nl = max_nl
            self.max_nc = max_nc
            self.nl += diff_l
            self.nc += diff_c
            for child in self.children:
                child.update(state, keypress)
        elif not self.active:
            for child in self.children:
                child.update(state, keypress)
        elif keypress == ord(':'):
            entered = self.query_cmd(':')
            # handle exits
            if entered == 'q' or entered == 'quit':
                state['quit'] = True
            elif entered == 'w' or entered == 'write':
                self.hard_delete_entries()
                state['write'] = True
            elif entered == 'c' or entered == 'comment':
                entered = self.query_cmd('enter comment: ')
                self.comment = self.entry['comment'] = entered.strip()
        elif keypress == ord('/'):
            entered = self.query_cmd('/')
            self.search(entered)
        elif keypress == ord('i') or keypress == ord('I'):
            if len(self.items) > 0:
                entered = self.query_cmd('edit entry: ', self.get_key(self.selected()))
                self.update_entry(entered)
            else:
                self.notify_cmd('insert failed: cannot update entry in empty list')
        elif keypress == ord('o') or keypress == ord('O'):
            entered = self.query_cmd('new entry: ')
            self.insert_entry(entered, keypress == ord('o'))
        elif keypress == ord('t') or keypress == ord('T'):
            entered = self.query_cmd('new entry: ', today())
            self.insert_entry(entered)
        elif keypress == ord('d') or keypress == ord('D'):
            self.soft_delete_selected_entry()
        elif keypress == ord('u'):
            self.undo_soft_delete()
        elif keypress == ord('s'):
            self.toggle_sorting()
        # handle navigation down
        elif keypress == ord('j') or keypress == curses.KEY_DOWN:
            self.cursor_pos = min(len(self.items) - 1, self.cursor_pos + 1)
        # handle navigation up
        elif keypress == ord('k') or keypress == curses.KEY_UP:
            self.cursor_pos = max(0, self.cursor_pos - 1)
        # handle navigation to top
        elif keypress == ord('g'):
            self.cursor_pos = 0
        # handle navigation to bottom
        elif keypress == ord('G'):
            self.cursor_pos = len(self.items) - 1
        # handle navigation right
        elif keypress == ord('l'):
            sel = self.selected()
            if self and not self.leaf:
                self.deactivate()
                self.add_child_window()
        # handle navigation left
        elif keypress == ord('h'):
            if self.parent:
                self.deletion_queue.clear()
                self.deactivate()
                self.parent.activate()
                self.parent.remove_child_window(self)
    
    def get_filtered_item_slice(self, items, start, end):
        soft_delete_marker = '***'
        new_list = []
        for i, item in enumerate(items[start:end]):
            if i + start in self.deletion_queue:
                new_list.append(soft_delete_marker if self.leaf else {'key': soft_delete_marker})
            else:
                new_list.append(item)
        return new_list

    def get_item_view(self):
        il = len(self.items)
        ll = self.nl - self.metadata_columns
        if il < ll:
            return self.cursor_pos, self.get_filtered_item_slice(self.items, 0, len(self.items))

        hnl = ll // 2
        el = ll % 2
        cp = self.cursor_pos
        if cp < hnl:
            cursor_view = cp
            item_view = self.get_filtered_item_slice(self.items, 0, ll)
            return cursor_view, item_view
        elif cp > il - hnl - el:
            cursor_view = -(il - cp) + ll
            item_view = self.get_filtered_item_slice(self.items, il - ll, il)
            return cursor_view, item_view
        else:
            cursor_view = hnl
            item_view = self.get_filtered_item_slice(self.items, cp - hnl, cp - hnl + ll)
            return cursor_view, item_view

    def render_item(self, item, pos, style):
        text = item if self.leaf else item['key']
        self.window.addstr(pos, 0, str(text)[:self.nc], style)

    def render(self):
        self.window.erase()

        # draw window dimensions and other metadata
        if self.display_metadata:
            # upper line
            self.window.hline(self.nl - 4, 0, '-', self.nc)
            self.window.addstr(self.nl - 4, 0, '+')
            # optional comment
            self.window.addstr(self.nl - 3, 0, self.comment if self.comment else '')
            # selected item highlight
            if self.leaf and len(self.items) > 0:
                self.window.addstr(self.nl - 2, 0, str(self.selected())[:self.nc])
            #self.window.addstr(self.nl - 2, 0, f'{self.nl}x{self.nc}'[:self.nc], curses.A_REVERSE)
            self.window.hline(self.nl - 1, 0, '-', self.nc)
            # lower line
            self.window.addstr(self.nl - 1, 0, '+')

        # draw items (subset if necessary)
        cursor, items = self.get_item_view()
        for i, item in enumerate(items):
            # draw item
            self.render_item(item, i, curses.A_REVERSE if i == cursor else curses.A_NORMAL)

        self.window.refresh()

        for child in self.children:
            child.render()
